package org.hamster.common.aggrid;

import org.hamster.common.aggrid.model.FilterModel;
import org.hamster.common.aggrid.model.GetRowsRequest;
import org.hamster.common.aggrid.model.GetRowsResponse;
import org.hamster.common.aggrid.model.SortModel;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Service class for handling ag-Grid requests with JPA Criteria API
 * 
 * @param <T> The entity type being handled
 */
public class JpaAgGridService<T> {
    
    private final EntityManager entityManager;
    private final Class<T> entityClass;
    
    public JpaAgGridService(EntityManager entityManager, Class<T> entityClass) {
        this.entityManager = entityManager;
        this.entityClass = entityClass;
    }
    
    /**
     * Process the get rows request from ag-Grid using JPA Criteria API
     * 
     * @param request The request from ag-Grid
     * @return Response with data for the requested block
     */
    public GetRowsResponse getRows(GetRowsRequest request) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = cb.createQuery(entityClass);
        Root<T> root = query.from(entityClass);
        
        // Apply filtering
        Predicate filterPredicate = createFilterPredicate(cb, root, request.getFilterModel());
        if (filterPredicate != null) {
            query.where(filterPredicate);
        }
        
        // Apply sorting
        List<Order> orders = createSortOrders(cb, root, request.getSortModel());
        if (!orders.isEmpty()) {
            query.orderBy(orders);
        }
        
        // Create count query for total row count
        CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
        Root<T> countRoot = countQuery.from(entityClass);
        countQuery.select(cb.count(countRoot));
        
        Predicate countPredicate = createFilterPredicate(cb, countRoot, request.getFilterModel());
        if (countPredicate != null) {
            countQuery.where(countPredicate);
        }
        
        TypedQuery<Long> countTypedQuery = entityManager.createQuery(countQuery);
        Long totalRows = countTypedQuery.getSingleResult();
        
        // Create data query with pagination
        TypedQuery<T> typedQuery = entityManager.createQuery(query);
        typedQuery.setFirstResult(request.getStartRow());
        typedQuery.setMaxResults(request.getEndRow() - request.getStartRow());
        
        List<T> rowsThisBlock = typedQuery.getResultList();
        
        // Determine if this is the last row
        boolean lastRow = request.getEndRow() >= totalRows;
        
        return new GetRowsResponse(rowsThisBlock, totalRows.intValue(), lastRow);
    }
    
    /**
     * Create filter predicate based on filter model
     */
    private Predicate createFilterPredicate(CriteriaBuilder cb, Root<T> root, Map<String, FilterModel> filterModel) {
        if (filterModel == null || filterModel.isEmpty()) {
            return null;
        }
        
        List<Predicate> predicates = new ArrayList<>();
        
        // Create predicate for each field
        for (Map.Entry<String, FilterModel> entry : filterModel.entrySet()) {
            String field = entry.getKey();
            FilterModel model = entry.getValue();
            
            Predicate fieldPredicate = createFieldPredicate(cb, root, field, model);
            if (fieldPredicate != null) {
                predicates.add(fieldPredicate);
            }
        }
        
        if (predicates.isEmpty()) {
            return null;
        }
        
        if (predicates.size() == 1) {
            return predicates.get(0);
        }
        
        // Combine with AND logic
        return cb.and(predicates.toArray(new Predicate[0]));
    }
    
    /**
     * Create predicate for a specific field
     */
    private Predicate createFieldPredicate(CriteriaBuilder cb, Root<T> root, String field, FilterModel model) {
        // Handle composite filters (AND/OR conditions)
        if (model.getConditions() != null && !model.getConditions().isEmpty()) {
            return createCompositeFilterPredicate(cb, root, field, model);
        }
        
        // Handle simple filter
        String filterType = model.getFilterType();
        String type = model.getType();
        Object filterValue = model.getFilter();
        
        return createSimplePredicate(cb, root, field, filterType, type, filterValue);
    }
    
    /**
     * Create a predicate for composite filters (with AND/OR conditions)
     */
    private Predicate createCompositeFilterPredicate(CriteriaBuilder cb, Root<T> root, String field, FilterModel model) {
        List<FilterModel> conditions = model.getConditions();
        String operator = model.getOperatorComposite();
        
        if (conditions.isEmpty()) {
            return null;
        }
        
        List<Predicate> predicates = new ArrayList<>();
        for (FilterModel condition : conditions) {
            Predicate predicate = createFieldPredicate(cb, root, field, condition);
            if (predicate != null) {
                predicates.add(predicate);
            }
        }
        
        if (predicates.isEmpty()) {
            return null;
        }
        
        if ("OR".equalsIgnoreCase(operator)) {
            return cb.or(predicates.toArray(new Predicate[0]));
        } else {
            // Default to AND
            return cb.and(predicates.toArray(new Predicate[0]));
        }
    }
    
    /**
     * Create a simple predicate based on filter parameters
     */
    private Predicate createSimplePredicate(CriteriaBuilder cb, Root<T> root, String field, 
                                          String filterType, String type, Object filterValue) {
        if (filterValue == null) {
            return null;
        }
        
        switch (filterType) {
            case "text":
                return createTextPredicate(cb, root, field, type, (String) filterValue);
            case "number":
                return createNumberPredicate(cb, root, field, type, filterValue);
            case "date":
                return createDatePredicate(cb, root, field, type, filterValue);
            default:
                return null;
        }
    }
    
    /**
     * Create text filter predicate
     */
    private Predicate createTextPredicate(CriteriaBuilder cb, Root<T> root, String field, String type, String filter) {
        Path<String> path = root.get(field);
        
        switch (type) {
            case "equals":
                return cb.equal(path, filter);
            case "notEqual":
                return cb.notEqual(path, filter);
            case "contains":
                return cb.like(path, "%" + filter + "%");
            case "notContains":
                return cb.notLike(path, "%" + filter + "%");
            case "startsWith":
                return cb.like(path, filter + "%");
            case "endsWith":
                return cb.like(path, "%" + filter);
            default:
                return null;
        }
    }
    
    /**
     * Create number filter predicate
     */
    private Predicate createNumberPredicate(CriteriaBuilder cb, Root<T> root, String field, String type, Object filter) {
        Path<Number> path = root.get(field);
        
        if (!(filter instanceof Number)) {
            return null;
        }
        
        Number filterNumber = (Number) filter;
        
        switch (type) {
            case "equals":
                return cb.equal(path, filterNumber);
            case "notEqual":
                return cb.notEqual(path, filterNumber);
            case "greaterThan":
                return cb.gt(path, filterNumber);
            case "greaterThanOrEqual":
                return cb.ge(path, filterNumber);
            case "lessThan":
                return cb.lt(path, filterNumber);
            case "lessThanOrEqual":
                return cb.le(path, filterNumber);
            default:
                return null;
        }
    }
    
    /**
     * Create date filter predicate
     */
    @SuppressWarnings("rawtypes")
    private Predicate createDatePredicate(CriteriaBuilder cb, Root<T> root, String field, String type, Object filter) {
        Path path = root.get(field);
        
        switch (type) {
            case "equals":
                return cb.equal(path, filter);
            case "notEqual":
                return cb.notEqual(path, filter);
            case "greaterThan":
                return cb.greaterThan(path, (Comparable) filter);
            case "lessThan":
                return cb.lessThan(path, (Comparable) filter);
            case "greaterThanOrEqual":
                return cb.greaterThanOrEqualTo(path, (Comparable) filter);
            case "lessThanOrEqual":
                return cb.lessThanOrEqualTo(path, (Comparable) filter);
            default:
                return null;
        }
    }
    
    /**
     * Create sort orders based on sort model
     */
    private List<Order> createSortOrders(CriteriaBuilder cb, Root<T> root, Map<String, SortModel> sortModel) {
        List<Order> orders = new ArrayList<>();
        
        if (sortModel == null || sortModel.isEmpty()) {
            return orders;
        }
        
        for (Map.Entry<String, SortModel> entry : sortModel.entrySet()) {
            String field = entry.getKey();
            SortModel sort = entry.getValue();
            
            Path<?> path = root.get(field);
            if ("desc".equals(sort.getSort())) {
                orders.add(cb.desc(path));
            } else {
                orders.add(cb.asc(path));
            }
        }
        
        return orders;
    }
}